#include <string.h>
#include <math.h>
#include <gst/gst.h>
#include <glib.h>
#include <glib/gprintf.h>
#include <gst/app/gstappsrc.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BUFFER_SIZE (1024 * 4)
#define MAX_LOOP_COUNT_WHEN_NEED_DATA (1024)

static pthread_mutex_t s_writeLock = PTHREAD_MUTEX_INITIALIZER;
static GstState s_appSrcState = GST_STATE_PAUSED;
static pthread_cond_t s_condForOpen = PTHREAD_COND_INITIALIZER;
static pthread_t s_gstThread = -1;
static gboolean s_isGstThreadStarted = FALSE;
static int s_gstThreadReturnVal = 0;

typedef struct NMHandler {
    GstElement *pipeline;
    GstElement *source;
    GMainLoop  *loop;
    int width;
    int height;
} NMHandler;

static NMHandler* s_handle = NULL;

static void appNeedData(GstAppSrc *src, guint length, gpointer user_data)
{
//    g_printf("####################### appNeedData enter!\n");
}

static void appEnoughData(GstAppSrc *src, gpointer user_data)
{
//    g_printf("####################### appEnoughData enter!\n");
}

static gboolean appSeekData(GstAppSrc *src, guint64 offset, gpointer user_data)
{
//    g_printf("####################### appSeekData enter!\n");
    return FALSE;
}

static GstAppSrcCallbacks s_callBacks = {
    appNeedData,
    appEnoughData,
    appSeekData
};

static gboolean bus_call (GstBus *bus,GstMessage *msg, gpointer data){
    GMainLoop *loop = (GMainLoop *) data;

    switch (GST_MESSAGE_TYPE (msg)) {

    case GST_MESSAGE_EOS:
        g_print ("Stream Ends\n");
        g_main_loop_quit (loop);
        break;

    case GST_MESSAGE_ERROR: {
        gchar  *debug;
        GError *error;

        gst_message_parse_error (msg, &error, &debug);
        g_free (debug);

        g_printerr ("Error: %s\n", error->message);
        g_error_free (error);

        g_main_loop_quit (loop);
        break;
    }
    case GST_MESSAGE_STATE_CHANGED: {
        GstState oldState;
        GstState newState;
        GstState pendingState;
        gst_message_parse_state_changed(msg, &oldState, &newState, &pendingState);
        g_printf("oldState:%d, newState:%d, pendingState:%d!\n", oldState, newState, pendingState);
        break;
    }
    default:
        break;
    }

    return TRUE;
}

static void initArguments() {
    s_gstThread = -1;
    s_appSrcState = GST_STATE_PAUSED;
}

static void on_pad_added (GstElement *element,
                          GstPad     *pad,
                          gpointer    data)
{
    g_printf("####################### on_pad_added --> start!\n");
    GstElement *video = (GstElement *) data;
    GstCaps *caps;
    GstStructure *str;
    GstPad *videoPad;

    /* only link once */
    videoPad = gst_element_get_static_pad (video, "sink");
    if (GST_PAD_IS_LINKED (videoPad)) {
        g_object_unref (videoPad);
        return;
    }

    /* check media type */
    caps = gst_pad_get_caps (pad);
    str = gst_caps_get_structure (caps, 0);
    if (!g_strrstr (gst_structure_get_name (str), "video")) {
        gst_caps_unref (caps);
        gst_object_unref (videoPad);
        return;
    }
    gst_caps_unref (caps);

    /* link'n'play */
    gst_pad_link (pad, videoPad);

    g_object_unref (videoPad);

    gst_element_set_state (s_handle->pipeline, GST_STATE_PLAYING);

    g_printf("####################### on_pad_added --> end!\n");
}

static void* gstThreadMain(void * args)
{
    GstBus *bus;

    NMHandler* hdl = (NMHandler*)args;

    /* Initializing GStreamer */
    gst_init (0, 0);
    hdl->loop = g_main_loop_new (NULL, FALSE);

//    GstCaps* caps = gst_caps_new_simple("application/x-rtp",
//                                        "media", G_TYPE_STRING, "video",
//                                        "clock-rate", G_TYPE_INT, 90000,
//                                        "encoding-name", G_TYPE_STRING, "MP2T-ES",
//                                        "payload", G_TYPE_INT, 96,
//                                        "clock-base", G_TYPE_UINT, 520513122,
//                                        NULL);

    g_printf("####################### gstThreadMain pipeline new!\n");
    hdl->pipeline = gst_pipeline_new ("video player");
    g_assert (hdl->pipeline);

    g_printf("####################### gstThreadMain make appsrc!\n");
    hdl->source   = gst_element_factory_make ("appsrc", "Source");
    g_assert (hdl->source);

//    g_printf("####################### gstThreadMain make capsfilter!\n");
//    GstElement *capsfilter = gst_element_factory_make ("capsfilter", "filter");
//    g_assert (capsfilter);

//    g_printf("####################### gstThreadMain make rtpmp2tdepay!\n");
//    GstElement *depay = gst_element_factory_make ("rtpmp2tdepay", "mp2tdepay") ;
//    g_assert (depay);

    g_printf("####################### gstThreadMain make mpegtsdemux!\n");
    GstElement * demux = gst_element_factory_make ("mpegtsdemux", "demux");
    g_assert (demux);

    GstElement* videovv = gst_bin_new ("video");
    g_assert (videovv);

    g_printf("####################### gstThreadMain make queue!\n");
    GstElement* videoQue = gst_element_factory_make ("queue", "videoQue");
    g_assert (videoQue);

    g_printf("####################### gstThreadMain make ffdec_h264!\n");
    GstElement* videoDecoder = gst_element_factory_make ("ffdec_h264", "decoder");
    g_assert (videoDecoder);

    g_printf("####################### gstThreadMain make xvimagesink!\n");
    GstElement* videosink = gst_element_factory_make ("xvimagesink", "videosink") ;
    g_assert (videosink);

    /*Make sure: Every elements was created ok*/
    if (!hdl->pipeline
            || !hdl->source
//            || !capsfilter
//            || !depay
            || !demux
            || !videoQue
            || !videoDecoder
            || !videosink) {
        g_printerr ("One of the elements wasn't create... Exiting\n");
        s_gstThreadReturnVal = -1;
        return (void*)&s_gstThreadReturnVal;
    }

    GstAppSrc* appsrc = GST_APP_SRC(hdl->source);
    gst_app_src_set_callbacks(appsrc, &s_callBacks, hdl, 0);

    /* Set video Source */
//    g_object_set (G_OBJECT (capsfilter), "caps", caps, NULL);

    g_object_set (G_OBJECT (videoQue), "max-size-buffers", 0, NULL);
    g_object_set (G_OBJECT (videoQue), "max-size-time", 0, NULL);
    g_object_set (G_OBJECT (videoQue), "max-size-bytes", 0, NULL);

    g_printf("####################### gstThreadMain watch bus!\n");
    /* Putting a Message handler */
    bus = gst_pipeline_get_bus (GST_PIPELINE (hdl->pipeline));
    gst_bus_add_watch (bus, bus_call, hdl->loop);
    gst_object_unref (bus);

    GstPad* videoQuePad = gst_element_get_static_pad (videoQue, "sink");

    gst_bin_add_many (GST_BIN (videovv), videoQue, videoDecoder, videosink, NULL);

    gst_element_add_pad (videovv, gst_ghost_pad_new ("sink", videoQuePad));
    gst_object_unref (videoQuePad);

    g_printf("####################### gstThreadMain add link!\n");
    /* Add Elements to the Bin */
    gst_bin_add_many (GST_BIN (hdl->pipeline),
                      hdl->source,
//                      capsfilter,
//                      depay,
                      demux,
//                      videoQue,
//                      videoDecoder,
//                      videosink,
                      NULL);

    g_signal_connect (demux, "pad-added", G_CALLBACK (on_pad_added), videovv);

    if (!gst_element_link_many(hdl->source, /*capsfilter,*/ /*depay,*/ demux, NULL)) {
        g_warning ("Linking src to ... to demux Fail...");
    }

    if(!gst_element_link_many(videoQue, videoDecoder, videosink, NULL)) {
        g_warning ("Linking videoQue to ... to videosink Fail...");
    }

    gst_bin_add (GST_BIN (hdl->pipeline), videovv);

    /* Run the pipeline */
    gst_element_set_state (hdl->pipeline, GST_STATE_PLAYING);
    gst_element_set_state (videovv, GST_STATE_PLAYING);

    s_isGstThreadStarted = TRUE;

    pthread_mutex_lock(&s_writeLock);
    pthread_cond_signal(&s_condForOpen);
    pthread_mutex_unlock(&s_writeLock);

    g_printf("####################### gstThreadMain entering loop!\n");
    g_main_loop_run (hdl->loop);

    /* Ending Playback */
    g_print ("End of the Streaming... ending the playback\n");
    gst_element_set_state (hdl->pipeline, GST_STATE_NULL);

    /* Eliminating Pipeline */
    g_print ("Eliminating Pipeline\n");
    gst_object_unref (GST_OBJECT (hdl->pipeline));

    s_isGstThreadStarted = FALSE;

    s_gstThreadReturnVal = 0;
    return (void*)&s_gstThreadReturnVal;
}

void* NMDioVideoOpen(int width, int height) {
    if (!s_isGstThreadStarted) {
        pthread_mutex_init(&s_writeLock, NULL);
        pthread_cond_init(&s_condForOpen, NULL);

        s_handle = (NMHandler*)malloc(sizeof(NMHandler));
        memset(s_handle, 0, sizeof(NMHandler));

        s_handle->width = width;
        s_handle->height = height;

        int ret = pthread_create(&s_gstThread, NULL, &gstThreadMain, (void*)s_handle);
        pthread_mutex_lock(&s_writeLock);

        pthread_detach(s_gstThread);

        if (0 != ret) {
            fprintf(stderr, "create gstThread failed!\n");
            pthread_mutex_unlock(&s_writeLock);
            return NULL;
        }
        pthread_cond_wait(&s_condForOpen, &s_writeLock);
        pthread_mutex_unlock(&s_writeLock);
    }

    return s_handle;
}

void NMDioVideoDisplay(void *argHandle, const char *packetData, int
                       packetSize)
{
    if (!argHandle) {
        g_printf("argHandle is NULL!\n");
        return;
    }

//    fprintf(stdout, "####################### NMDioVideoDisplay begin!\n");
    NMHandler* hdl = (NMHandler*)argHandle;
    if (!hdl) {
        GST_ERROR_OBJECT(hdl, "hdl is invalid, donothing!\n");
        return;
    }

    GstAppSrc* src = GST_APP_SRC(hdl->source);

//    fprintf(stdout, "####################### NMDioVideoDisplay creating Buffer!\n");
    GstBuffer* buffer = gst_buffer_new_and_alloc(packetSize);

//    fprintf(stdout, "####################### NMDioVideoDisplay copy data to Buffer!\n");
    memcpy(GST_BUFFER_DATA(buffer), packetData, packetSize);
    GST_BUFFER_OFFSET(buffer) = 0;
    GST_BUFFER_OFFSET_END(buffer) = packetSize;

    GstFlowReturn ret = gst_app_src_push_buffer(src, buffer);
//    fprintf(stdout, "####################### NMDioVideoDisplay push buffer, result(%d)!\n", ret);

    if (ret != GST_FLOW_OK && ret != GST_FLOW_UNEXPECTED) {
        GST_ELEMENT_ERROR(src, CORE, FAILED, (0), (0));
    }

//    fprintf(stdout, "####################### NMDioVideoDisplay end!\n");
}

void NMDioVideoClose(void *argHandle)
{
    fprintf(stdout, "NMDioVideoClose --> start!\n");
    if (!argHandle) {
        g_printf("argHandle is NULL!\n");
        return;
    }

    NMHandler* hdl = (NMHandler*)argHandle;
    if (s_isGstThreadStarted) {
        g_main_loop_quit (hdl->loop);
    }

    if (s_handle) {
        free(s_handle);
        s_handle = NULL;
    }
    fprintf(stdout, "NMDioVideoClose --> end!\n");
}

int main(int argc, char* argv[]) {
    char filename[128];
    memset(filename, 0, sizeof(filename));
    if (2 > argc) {
        printf("please input filename:");
        fgets(filename, 128, stdin);
        int lenth = strlen(filename);
        filename[lenth - 1] = 0;
    }
    else {
        strncpy(filename, argv[1], 128);
    }

    int fileinfd = open(filename, O_RDONLY);
    if (0 > fileinfd) {
        printf("file(%s) is not exist or cannot open!\n", filename);
        return 1;
    }

    char buf[1024];
    int readsize = 0;
    void* hdl = NMDioVideoOpen(800, 480);
    do {
        readsize = read(fileinfd, buf, 1024);
        if (0 < readsize) {
            NMDioVideoDisplay(hdl, buf, readsize);
//            sleep(1);
        }
    } while(readsize >= 0);

    NMDioVideoClose(hdl);
    return 0;
}
